#!/bin/bash
#
# weak-modules - determine which modules are kABI compatible with installed
#                kernels and set up the symlinks in /lib/*/weak-updates.
#

# Changelog:
#
# 2006/12/11 - Updated logic for determining the system's initrd location,
#              to account for IA64 differences. (#215432)

unset LANG LC_ALL LC_COLLATE

tmpdir=$(mktemp -td ${0##*/}.XXXXXX)
trap "rm -rf $tmpdir" EXIT
unset ${!changed_modules_*} ${!changed_initrd_*}

if [ "ia64" == `uname -m` ];
then
	initrd_prefix="/boot/efi/EFI/redhat"
else
	initrd_prefix="/boot"
fi

#!/bin/sh

# rpmsort: The sort in coreutils can't sort the RPM list how we want it so we
# instead transform the list into a form it will sort correctly, then sort.
rpmsort() {

	rpmlist=($(cat))

	if [ "$1" == "-r" ]; then
		reverse=1
	else
		reverse=0
	fi

        if [ ${#rpmlist[@]} = 1 ]; then
        # By definition already sorted
		echo ${rpmlist[@]}
                return
        fi

        # Hash every value in the rpmlist
        for ((i = 0; i < ${#rpmlist[@]}; i++)); do
                rpmhash[i]="$(echo ${rpmlist[i]} | awk '{decode="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-."; split($1,tmp,"");for (i=1;i<=length($1);i++){ printf "%02x",index(decode,tmp[i]) } }')"
        done

        for ((i = 0; i < ${#rpmlist[@]}; i++)); do

                for ((j = 0; j < ${#rpmlist[@]}-1; j++)); do

                        rpm1=${rpmlist[j]}      rpm2=${rpmlist[j+1]}
                        hex1=${rpmhash[j]}      hex2=${rpmhash[j+1]}

                        if [ ${#rpm1} -lt ${#rpm2} ]; then
                                len=$((${#rpm1}*2));
                        else
                                len=$((${#rpm2}*2));
                        fi

		# BZ249537 - don't use head -n 1 to rip out the first line
		# and instead use a read loop to do it. There are better
		# ways to do this, but this solution works too :-).

                        if [ "$(printf "%s\n" ${hex1:0:$len} ${hex2:0:$len} \
                              | sort | (read input; echo "$input"; \
                                        while read input; do true; done))" = \
                                "$hex2" ]; then
                                rpmlist[j]=$rpm2; rpmlist[j+1]=$rpm1
                                rpmhash[j]=$hex2; rpmhash[j+1]=$hex1
                        fi
                done
        done

	if [ $reverse = 0 ]; then
        	for ((n = 0; n < ${#rpmlist[@]} ; n++)); do
                	echo "${rpmlist[$n]}"
        	done
	else
		for ((n = ${#rpmlist[@]}-1; n >= 0; n--)); do
			echo "${rpmlist[$n]}"
		done
	fi
}

# read_modules_list:
# Read in a list of modules from standard input. Convert the filenames into
# absolute paths and compute the kernel release for each module (either using
# the modinfo section or through the absolute path.
read_modules_list() {
    local IFS=$'\n'
    modules=($(cat))

    for ((n = 0; n < ${#modules[@]}; n++)); do
        if [ ${modules[n]:0:1} != '/' ]; then
            modules[n]="$PWD/${modules[n]}"
        fi
        if [ -f "${modules[n]}" ]; then
            module_krels[n]=$(krel_of_module ${modules[n]})
        else
            # Try to extract the kernel release from the path
            set -- "${modules[n]#/lib/modules/}"
            module_krels[n]=${1%%/*}
        fi
    done
}

# read_old_initrd:
compare_initrd_modules() {
    local old_initrd=$1
    local new_initrd=$2

    rm -rf "$tmpdir/old_initrd"
    rm -rf "$tmpdir/new_initrd"
    mkdir "$tmpdir/old_initrd"
    mkdir "$tmpdir/new_initrd"

    pushd "$tmpdir/old_initrd" >/dev/null
    zcat "$old_initrd" | cpio -i 2>/dev/null
    n=0; for i in `find . -iname \*.ko|sort`; do
        old_initrd_modules[n]="$i"
        n=$((n+1))
    done
    popd >/dev/null

    pushd "$tmpdir/new_initrd" >/dev/null
    zcat "$new_initrd" | cpio -i 2>/dev/null
    n=0; for i in `find . -iname \*.ko|sort`; do
        new_initrd_modules[n]="$i"
        n=$((n+1))
    done
    popd >/dev/null

    if [ "${#old_initrd_modules[@]}" == "${#new_initrd_modules[@]}" ];
    then
        for ((n = 0; n < ${#old_initrd_modules[@]}; n++)); do
            old_md5=`md5sum $tmpdir/old_initrd/${old_initrd_modules[n]}|sed -nre 's:(^\ )* .*:\1:p'`
            new_md5=`md5sum $tmpdir/new_initrd/${new_initrd_modules[n]}|sed -nre 's:(^\ )* .*:\1:p'`
            if [ ! "$old_md5" == "$new_md5" ];
            then
                return 1
            fi
        done
    else
        return 1
    fi

    return 0
}

# check_initrd:
check_initrd() {
    local kernel=$1
    local kernel_is_xen=0
    local xen_kverrel=0

    if [ "xen" == "`echo $kernel|sed -nre 's:^.*(xen)$:\1:p'`" ];
    then
        kernel_is_xen=1
        xen_kverrel="`echo $kernel|sed -nre 's:^(.*)xen$:\1:p'`"
    else
        kernel_is_xen=0
    fi

    # This logic probably isn't needed. When are we ever actually likely to have
    # an unbootable system, with no kernel, *before* we run this script? :-)
    if [ ! -e "$initrd_prefix/initrd-$kernel.img" ];
    then
        new_initrd="$initrd_prefix/initrd-$kernel.img"
        if [ "$kernel_is_xen" == "0" ]; then
            /sbin/new-kernel-pkg --mkinitrd --initrdfile="$new_initrd" --depmod --install "$kernel"
        else
            /sbin/new-kernel-pkg --mkinitrd --initrdfile="$new_initrd" --depmod --install --multiboot="$initrd_prefix/xen.gz-$xen_kverrel" "$kernel"
        fi
    else
        old_initrd="$initrd_prefix/initrd-$kernel.img"
        tmp_initrd="$initrd_prefix/initrd-$kernel.tmp"
        new_initrd="$initrd_prefix/initrd-$kernel.img"

        /sbin/mkinitrd --allow-missing -f "$tmp_initrd" "$kernel"

        if ! $(compare_initrd_modules "$old_initrd" "$tmp_initrd");
        then
            mv "$old_initrd" "$old_initrd".dup_orig
            mv "$tmp_initrd" "$new_initrd"

            if [ "$kernel_is_xen" == "0" ]; then
                /sbin/new-kernel-pkg --initrdfile="$new_initrd" --depmod --install "$kernel"
            else
                /sbin/new-kernel-pkg --initrdfile="$new_initrd" --depmod --install --multiboot="$initrd_prefix/xen.gz-$xen_kverrel" "$kernel"
            fi
        else
            rm -f "$tmp_initrd"
        fi
    fi
}

# krel_of_module:
# Compute the kernel release of a module.
krel_of_module() {
    declare module=$1
    /sbin/modinfo -F vermagic "$module" | awk '{print $1}'
}

# module_is_compatible:
# Determine if a module is compatible with a particular kernel release. Also
# include any symbol deps that might be introduced by other external KMPs.
module_is_compatible() {
    declare module=$1 krel=$2 module_krel=$(krel_of_module "$module")

    if [ ! -e "$tmpdir/all-symvers-$krel-$module_krel" ]; then
        # Symbols exported by the "new" kernel
        if [ ! -e $tmpdir/symvers-$krel ]; then
            if [ -e /boot/symvers-$krel.gz ]; then
                zcat /boot/symvers-$krel.gz \
                | sed -r -ne 's:^(0x[0]*[0-9a-f]{8}\t[0-9a-zA-Z_]+)\t.*:\1:p'
            fi > $tmpdir/symvers-$krel
        fi

        # Symbols that other add-on modules of the "old" kernel export
        # (and that this module may require)
        if [ ! -e "$tmpdir/extra-symvers-$module_krel" ]; then
            if [ -e /lib/modules/$module_krel/extra ]; then
                find /lib/modules/$module_krel/extra -name '*.ko' \
                | xargs nm \
                | sed -nre 's:^[0]*([0-9a-f]{8}) A __crc_(.*):0x\1 \2:p'
            fi > $tmpdir/extra-symvers-$module_krel
        fi

        sort -u $tmpdir/symvers-$krel $tmpdir/extra-symvers-$module_krel \
        > "$tmpdir/all-symvers-$krel-$module_krel"
    fi

    # If the module does not have modversions enabled, $tmpdir/modvers
    # will be empty.
    /sbin/modprobe --dump-modversions "$module" \
    | sed -r -e 's:^(0x[0]*[0-9a-f]{8}\t.*):\1:' \
    | sort -u \
    > $tmpdir/modvers

    # Only include lines of the second file in the output that don't
    # match lines in the first file. (The default separator is
    # <space>, so we are matching the whole line.)
    join -j 1 -v 2 $tmpdir/all-symvers-$krel-$module_krel \
                   $tmpdir/modvers > $tmpdir/join

    if [ ! -s $tmpdir/modvers ]; then
        echo "Warning: Module ${module##*/} from kernel $module_krel has no" \
             "modversions, so it cannot be reused for kernel $krel" >&2
    elif [ -s $tmpdir/join ]; then
        [ -n "$verbose" ] &&
        echo "Module ${module##*/} from kernel $module_krel is not compatible" \             "with kernel $krel in symbols:" $(sed -e 's:.* ::' $tmpdir/join)
    else
        [ -n "$verbose" ] &&
        echo "Module ${module##*/} from kernel $module_krel is compatible" \
             "with kernel $krel"
        return 0
    fi
    return 1
}

# doit:
# A wrapper used whenever we're going to perform a real operation.
doit() {
    [ -n "$verbose" ] && echo "$@"
    [ -n "$dry_run" ] || "$@"
}

usage() {
    echo "Usage: ${0##*/} [options] {--add-modules|--remove-modules}"
    echo "${0##*/} [options] {--add-kernel|--remove-kernel} {kernel-release}"
    cat <<'EOF'
--add-modules
        Add a list of modules read from standard input. Create
        symlinks in compatible kernel's weak-updates/ directory.
        The list of modules is read from standard input.

--remove-modules
        Remove compatibility symlinks from weak-updates/ directories
        for a list of modules.  The list of modules is read from
        standard input. Optionally specify --delete-modules to
        prevent weak-modules from attempting to locate any
        compatible modules to replace those being removed.

--add-kernel
        Add compatibility symlinks for all compatible modules to the
        specified or running kernel.

--remove-kernel
        Remove all compatibility symlinks for the specified or current
        kernel.

--no-initrd
	Do not generate an initrd.

--verbose
        Print the commands executed.

--dry-run
        Do not create/remove any files.
EOF
    exit $1
}

# module_has_changed:
# Mark if an actual change occured that we need to deal with later by calling
# depmod or mkinitrd against the affected kernel.
module_has_changed() {

    declare module=$1 krel=$2

    module=${module%.ko}
    module=${module##*/}

    eval "changed_modules_${krel//[^a-zA-Z0-9]/_}=$krel"
    eval "changed_initrd_${krel//[^a-zA-Z0-9]/_}=$krel"

}

# add_modules:
# Read in a list of modules from stdinput and process them for compatibility
# with installed kernels under /lib/modules.
add_modules() {
    read_modules_list || exit 1
    if [ ${#modules[@]} -gt 0 ]; then
        for krel in $(ls /lib/modules/); do
            [ -e "/boot/symvers-$krel.gz" ] || continue
            for ((n = 0; n < ${#modules[@]}; n++)); do
                module="${modules[n]}"
                module_krel="${module_krels[n]}"
                case "$module" in
                /lib/modules/$krel/*)
                    # Module was built against this kernel, update initrd.
                    module_has_changed $module $krel
                    continue ;;
                esac

		# Module my also serve as a weak-update built against another
		# kernel. We need to create symlinks for compatible kernels
		# under /lib/modules and rerun depmod/mkinitrd for those.

		# BZ249537 - don't use head -n 1 to rip out the first line
		# and instead use a read loop to do it. There are better
		# ways to do this, but this solution works too :-).

                subpath="${module#/lib/modules/$module_krel/extra}"
                weak_module="/lib/modules/$krel/weak-updates/${subpath#/}"
                if [ -r "$weak_module" ]; then
                    weak_krel=$(krel_of_module "$weak_module")
                    if [ "$weak_krel" != "$module_krel" ] &&
                       [ "$(printf "%s\n" "$weak_krel" "$module_krel" \
                            | rpmsort | (read input; echo "$input"; \
                                         while read input; do true; done))" = \
                         "$module_krel" ]; then
                        # Keep modules from more recent kernels.
                        [ -n "$verbose" ] && echo \
"Keeping module ${module##*/} from kernel $weak_krel for kernel $krel"
                        continue
                    fi
                fi
                if module_is_compatible $module $krel; then
                    doit mkdir -p $(dirname $weak_module)
                    doit ln -sf $module $weak_module
                    # Module was built against another kernel, update initrd.
                    module_has_changed $module $krel
                fi
            done
        done
    fi
}

# remove_modules:
# Read in a list of modules from stdinput and process them for removal.
# Parameter is noreplace to delete modules, otherwise link compat.
remove_modules() {
    delete_modules=${1:-replace}

    read_modules_list || exit 1
    if [ ${#modules[@]} -gt 0 ]; then

	# Hunt for all known users of this module in /lib/modules, remove them
	# and create symlinks to other compatible modules (downgrade) if
	# possible, update initrd for each modified kernel too.

        krels=($(ls /lib/modules/ | rpmsort -r))
        for krel in "${krels[@]}"; do
            [ -e "/boot/symvers-$krel.gz" ] || continue
            for ((n = 0; n < ${#modules[@]}; n++)); do
                module="${modules[n]}"
                module_krel="${module_krels[n]}"

		# Module is going to be removed, update initrd.
		module_has_changed $module $krel

                subpath="${module#/lib/modules/$module_krel/extra}"
                weak_module="/lib/modules/$krel/weak-updates/${subpath#/}"
                if [ "$module" == "`readlink $weak_module`" ]; then
                    [ -n "$verbose" ] && echo \
"Removing compatible module ${module##*/} from kernel $krel"
                    doit rm -f "$weak_module"
                    if [ "replace" == "$delete_modules" ]; then
                        for krel2 in "${krels[@]}"; do
                            if [ $krel2 != $krel ]; then
                                module="/lib/modules/$krel2/extra/${subpath#/}"
                                [ -e "$module" ] || continue
                                if module_is_compatible "$module" "$krel"; then
                                    [ -n "$verbose" ] && echo \
"Adding compatible module ${module##*/} from kernel $krel2 instead"
                                    doit ln -s "$module" "$weak_module"
			            module_has_changed $module $krel
                                    break
                                fi
                            fi
                        done
                    fi
                    doit rmdir --parents --ignore-fail-on-non-empty \
                               "$(dirname "$weak_module")"
                fi
            done
        done
    fi
}

add_kernel() {
    add_krel=${1:-$(uname -r)}
    if [ ! -e "/boot/symvers-$add_krel.gz" ]; then
        echo "Symvers dump file /boot/symvers-$add_krel.gz" \
             "not found" >&2
        exit 1
    fi
    for krel in $(ls /lib/modules/ | rpmsort -r); do
        [ "$add_krel" = "$krel" ] && continue
        [ -d /lib/modules/$krel/extra ] || continue
        for module in $(find /lib/modules/$krel/extra -name '*.ko'); do
            subpath="${module#/lib/modules/$krel/extra}"
            weak_module="/lib/modules/$add_krel/weak-updates/${subpath#/}"
            [ -e "$weak_module" ] && continue
            if module_is_compatible $module $add_krel; then
		module_has_changed $module $add_krel
                doit mkdir -p $(dirname $weak_module)
                doit ln -sf $module $weak_module
            fi
        done
    done
}

remove_kernel() {
    remove_krel=${1:-$(uname -r)}
    weak_modules="/lib/modules/$remove_krel/weak-updates"
    module_has_changed $weak_modules $remove_krel
    doit rm -rf "$weak_modules"
    doit rm -rf "$initrd_prefix/initrd-$remove_krel.img.dup_orig"
}

################################################################################
################################## MAIN GUTS ###################################
################################################################################

options=`getopt -o h --long help,add-modules,remove-modules \
                     --long add-kernel,remove-kernel \
                     --long dry-run,no-initrd,verbose,delete-modules -- "$@"`

[ $? -eq 0 ] || usage 1

eval set -- "$options"

while :; do
    case "$1" in
    --add-modules)
        do_add_modules=1
        ;;
    --remove-modules)
        do_remove_modules=1
        ;;
    --add-kernel)
        do_add_kernel=1
        ;;
    --remove-kernel)
        do_remove_kernel=1
        ;;
    --dry-run)
        dry_run=1
        ;;
    --no-initrd)
        no_initrd=1
        ;;
    --verbose)
        verbose=1
        ;;
    --delete-modules)
        do_delete_modules=1
        ;;
    -h|--help)
        usage 0
        ;;
    --)
        shift
        break
        ;;
    esac
    shift
done

if [ -n "$do_add_modules" ]; then
	add_modules

elif [ -n "$do_remove_modules" ]; then
        if [ -n "$do_delete_modules" ]; then
            remove_modules "noreplace"
        else
	    remove_modules
        fi

elif [ -n "$do_add_kernel" ]; then
	kernel=${1:-$(uname -r)}
	add_kernel $kernel

elif [ -n "$do_remove_kernel" ]; then
	kernel=${1:-$(uname -r)}
	remove_kernel $kernel

	exit 0
else
	usage 1
fi

################################################################################
###################### CLEANUP POST ADD/REMOVE MODULE/KERNEL ###################
################################################################################

# run depmod and mkinitrd as needed
for krel in ${!changed_modules_*}; do
    krel=${!krel}

    doit /sbin/depmod -ae -F /boot/System.map-$krel $krel
done

for krel in ${!changed_initrd_*}; do
    krel=${!krel}

    if [ ! -n "$no_initrd" ]; then
        check_initrd $krel
    fi
done
